const fs = require("fs"); // NodeJs-fs 用于与文件系统进行交互
const tf = require("@tensorflow/tfjs-node"); // TensorflowJs-Node

/* @Function    handleImageTensor()
 * @Description 处理图片资源以能够进入模型
 *              张量转换、调整尺寸、归一化、重塑形状
 * @Parameters  buffer 图片读取的进程/缓冲 */
const handleImage2Tensor = (buffer) => {
    /* tf.tidy() 通常将对操作的调用，包装到该函数中，用于自动清理内存，使用此方法有助于避免内存泄漏
     * 当训练数据量非常多时，尤其是图片资源，不仅存储很占空间，运行时也很耗内存，此时便很容易将内存冲爆
     * 对张量数据Tensor执行函数方法时，会产生很多中间变量和参数，因此经常使用该方法来及时清理内存 */
    return tf.tidy(() => {
        /* tf.node.decodeImage() 给定图像编码字节(支持BMP/GIF/JPEG/PNG)，返回解码图像的3D或4D张量(Tensor3D/4D)
         * 要求输入参数是Uint8Array(8位无符号整数数组)编码图像，因此使用New Uint8Array()转换图片buffer的数据类型 */
        const imageTs = tf.node.decodeImage(new Uint8Array(buffer));
        /* tf.image.resizeBilinear() 调整三维图像的尺寸大小，调整成新的形状
         * 在深度学习框架中，图片分类模型的输入，图像尺寸必须是224*224，原因参考：
         * https://blog.csdn.net/Mr_zhuo_/article/details/109219050 */
        const imageTsResize = tf.image.resizeBilinear(imageTs, [224,224]);
        const imageTsNormalize = imageTsResize.toFloat().sub(255/2).div(255/2); // 数据归一化处理(暂无资料)
        /* tf.reshape() 重塑张量形状。后续使用模型MobileNet，查看其图层结构，显示每层输出数据都形如[null,224,224,3]
         * 1表示在原有图像基础上，再向外拓展一层维度；224,224表示图像输入模型的尺寸；3表示是由RGB三基色组成的彩色图像 */
        return imageTsNormalize.reshape([1,224,224,3]); // 返回与输入张量相同值的新张量
    });
};

/* @Function    getTrainData()
 * @Description 获取模型的训练数据
 * @Parameters  dataDir [String] 训练数据图片目录
 *              saveDir [String] 程序输出文件目录 */
const getTrainData = async (dataDir, saveDir) => {
    /* fs.readdirSync() 同步读取目录内容，返回目录下所有文件/文件夹，名称合集的数组，但未排除无效内容(如.DS_Store)
     * category 训练数据目录下，各子目录(垃圾类别)名称合集的数组(["其他垃圾","厨余垃圾","可回收物","有害垃圾"]) */
    const category = fs.readdirSync(dataDir).filter(n => !n.includes(".")); // 过滤掉无效文件

    /* fs.writeFileSync() 同步写数据到文件，若文件不存在则创建，若文件已存在则覆盖 */
    /* 在后续使用模型执行预测分类时，模型返回的并非是类别名称的中文，而是名称在类别数组中的下标index(数字0123)
     * 要将数组下标映射到对应的中文名称，因此将类别名称数组转换成JSON字符串，并创建JSON文件存储起来方便取用 */
    const categorySavePath = `${saveDir}/category.json`; // model-save/category.json
    fs.writeFileSync(categorySavePath, JSON.stringify(category)); // 使用相对路径存储

    const imageTensorArr = []; // 存储各垃圾图片的图像张量(Tensor)
    const imageIndexArr = [];  // 存储各垃圾图片所属的类别(数组下标)

    category.forEach((dir, dirIndex) => {
        /* 依次读取各类别目录下的垃圾图片，获取其文件名称(如img_18235.jpg)
         * 正则匹配以jpg结尾(/jpg$/)的文件名称，过滤掉不符合不相关的文件 */
        fs.readdirSync(`${dataDir}/${dir}`).filter(name => name.match(/jpg$/))
            .slice(0,100) // 截取100项元素，即每种类别仅读取100张图片(加快功能流程执行)
            .forEach(filename => {
                console.log(`正在读取目录${dir}中图片文件${filename}`);
                /* imagePath 图片资源的完整路径拼接(如images/train/厨余垃圾/img_2205.jpg)
                 * fs.readFileSync() 同步读取文件全部内容，若未指定字符编码，则返回原始buffer
                 * 读取垃圾图片入内存进程(缓冲整个文件)，Buffer(缓冲器/缓冲寄存器)里存储起来 */
                const imagePath = `${dataDir}/${dir}/${filename}`; // 路径
                const imageBuffer = fs.readFileSync(imagePath);    // 读取
                /* 处理图片资源，转换成符合模型投喂的图像张量(Tensor)，并将处理好的结果依次保存起来，以创建模型输入
                 * 同时依次存储，该图片类别目录下标，以创建模型输出(根据已创建的类别JSON文件，获取对应的中文名称) */
                const imageTensor = handleImage2Tensor(imageBuffer);
                imageTensorArr.push(imageTensor); // 元素是图像Tensor
                imageIndexArr.push(dirIndex); // 元素是类别下标0|1|2|3
            });
    }); // 参数意义是各类别目录名称dir及其下标dirIndex

    /* tf.concat() 沿着给定轴连接数组中的张量，其等级类型必须匹配，大小也必须在除轴以外的所有维度上匹配
     * 此处是将数组中的各图像张量数据连接起来，形成更大更高维度的Tensor，其最终数据类型依旧是Tensor */
    /* tf.tensor() 使用提供的值、形状和数据类型，创建张量数据Tensor，此处是将普通数组转换成张量类型 */
    const inputTs = tf.concat(imageTensorArr); const outputTs = tf.tensor(imageIndexArr);
    return { inputTs, outputTs, category };
};
module.exports = getTrainData; // 导出方法
